﻿
using System;
using System.Runtime.InteropServices;
using System.Collections.Generic;
using System.Text;

namespace VPL
{
	//-------------------------------------------------------------
	// Interfaces defined by fusion
	//-------------------------------------------------------------
	[ComImport, InterfaceType(ComInterfaceType.InterfaceIsIUnknown), Guid("e707dcde-d1cd-11d2-bab9-00c04f8eceae")]
	internal interface IAssemblyCache
	{
		[PreserveSig()]
		int UninstallAssembly(
							int flags,
							[MarshalAs(UnmanagedType.LPWStr)]
                            String assemblyName,
							InstallReference refData,
							out AssemblyCacheUninstallDisposition disposition);

		[PreserveSig()]
		int QueryAssemblyInfo(
							int flags,
							[MarshalAs(UnmanagedType.LPWStr)]
                            String assemblyName,
							ref AssemblyInfo assemblyInfo);
		[PreserveSig()]
		int Reserved(
							int flags,
							IntPtr pvReserved,
							out Object ppAsmItem,
							[MarshalAs(UnmanagedType.LPWStr)]
                            String assemblyName);
		[PreserveSig()]
		int Reserved(out Object ppAsmScavenger);

		[PreserveSig()]
		int InstallAssembly(
							int flags,
							[MarshalAs(UnmanagedType.LPWStr)]
                            String assemblyFilePath,
							InstallReference refData);
	}// IAssemblyCache

	[ComImport, InterfaceType(ComInterfaceType.InterfaceIsIUnknown), Guid("CD193BC0-B4BC-11d2-9833-00C04FC31D2E")]
	internal interface IAssemblyName
	{
		[PreserveSig()]
		int SetProperty(
				int PropertyId,
				IntPtr pvProperty,
				int cbProperty);

		[PreserveSig()]
		int GetProperty(
				int PropertyId,
				IntPtr pvProperty,
				ref int pcbProperty);

		[PreserveSig()]
		int Finalize();

		[PreserveSig()]
		int GetDisplayName(
				StringBuilder pDisplayName,
				ref int pccDisplayName,
				int displayFlags);

		[PreserveSig()]
		int Reserved(ref Guid guid,
			Object obj1,
			Object obj2,
			String string1,
			Int64 llFlags,
			IntPtr pvReserved,
			int cbReserved,
			out IntPtr ppv);

		[PreserveSig()]
		int GetName(
				ref int pccBuffer,
				StringBuilder pwzName);

		[PreserveSig()]
		int GetVersion(
				out int versionHi,
				out int versionLow);
		[PreserveSig()]
		int IsEqual(
				IAssemblyName pAsmName,
				int cmpFlags);

		[PreserveSig()]
		int Clone(out IAssemblyName pAsmName);
	}// IAssemblyName

	[ComImport, InterfaceType(ComInterfaceType.InterfaceIsIUnknown), Guid("21b8916c-f28e-11d2-a473-00c04f8ef448")]
	internal interface IAssemblyEnum
	{
		[PreserveSig()]
		int GetNextAssembly(
				IntPtr pvReserved,
				out IAssemblyName ppName,
				int flags);
		[PreserveSig()]
		int Reset();
		[PreserveSig()]
		int Clone(out IAssemblyEnum ppEnum);
	}// IAssemblyEnum

	[ComImport, InterfaceType(ComInterfaceType.InterfaceIsIUnknown), Guid("582dac66-e678-449f-aba6-6faaec8a9394")]
	internal interface IInstallReferenceItem
	{
		// A pointer to a FUSION_INSTALL_REFERENCE structure. 
		// The memory is allocated by the GetReference method and is freed when 
		// IInstallReferenceItem is released. Callers must not hold a reference to this 
		// buffer after the IInstallReferenceItem object is released. 
		// This uses the InstallReferenceOutput object to avoid allocation 
		// issues with the interop layer. 
		// This cannot be marshaled directly - must use IntPtr 
		[PreserveSig()]
		int GetReference(
				out IntPtr pRefData,
				int flags,
				IntPtr pvReserced);
	}// IInstallReferenceItem

	[ComImport, InterfaceType(ComInterfaceType.InterfaceIsIUnknown), Guid("56b1a988-7c0c-4aa2-8639-c3eb5a90226f")]
	internal interface IInstallReferenceEnum
	{
		[PreserveSig()]
		int GetNextInstallReferenceItem(
				out IInstallReferenceItem ppRefItem,
				int flags,
				IntPtr pvReserced);
	}// IInstallReferenceEnum

	public enum AssemblyCommitFlags
	{
		Default = 1,
		Force = 2
	}// enum AssemblyCommitFlags

	public enum AssemblyCacheUninstallDisposition
	{
		Unknown = 0,
		Uninstalled = 1,
		StillInUse = 2,
		AlreadyUninstalled = 3,
		DeletePending = 4,
		HasInstallReference = 5,
		ReferenceNotFound = 6
	}

	[Flags]
	internal enum AssemblyCacheFlags
	{
		GAC = 2,
	}

	internal enum CreateAssemblyNameObjectFlags
	{
		CANOF_DEFAULT = 0,
		CANOF_PARSE_DISPLAY_NAME = 1,
	}

	[Flags]
	internal enum AssemblyNameDisplayFlags
	{
		VERSION = 0x01,
		CULTURE = 0x02,
		PUBLIC_KEY_TOKEN = 0x04,
		PROCESSORARCHITECTURE = 0x20,
		RETARGETABLE = 0x80,
		// This enum will change in the future to include
		// more attributes.
		ALL = VERSION
									| CULTURE
									| PUBLIC_KEY_TOKEN
									| PROCESSORARCHITECTURE
									| RETARGETABLE
	}

	[StructLayout(LayoutKind.Sequential)]
	public class InstallReference
	{
		public InstallReference(Guid guid, String id, String data)
		{
			flags = 0;
			// quiet compiler warning 
			if (flags == 0) { }
			guidScheme = guid;
			identifier = id;
			description = data;
		}

		public Guid GuidScheme
		{
			get { return guidScheme; }
		}

		public String Identifier
		{
			get { return identifier; }
		}

		public String Description
		{
			get { return description; }
		}

		int flags;
		Guid guidScheme;
		[MarshalAs(UnmanagedType.LPWStr)]
		String identifier;
		[MarshalAs(UnmanagedType.LPWStr)]
		String description;
	}

	[StructLayout(LayoutKind.Sequential)]
	internal struct AssemblyInfo
	{
		public int cbAssemblyInfo; // size of this structure for future expansion
		public int assemblyFlags;
		public long assemblySizeInKB;
		[MarshalAs(UnmanagedType.LPWStr)]
		public String currentAssemblyPath;
		public int cchBuf; // size of path buf.
	}

	[ComVisible(false)]
	public class InstallReferenceGuid
	{
		public static bool IsValidGuidScheme(Guid guid)
		{
			return (guid.Equals(UninstallSubkeyGuid) ||
					guid.Equals(FilePathGuid) ||
					guid.Equals(OpaqueGuid) ||
					guid.Equals(Guid.Empty));
		}

		public readonly static Guid UninstallSubkeyGuid = new Guid("8cedc215-ac4b-488b-93c0-a50a49cb2fb8");
		public readonly static Guid FilePathGuid = new Guid("b02f9d65-fb77-4f7a-afa5-b391309f11c9");
		public readonly static Guid OpaqueGuid = new Guid("2ec93463-b0c3-45e1-8364-327e96aea856");
		// these GUID cannot be used for installing into GAC.
		public readonly static Guid MsiGuid = new Guid("25df0fc1-7f97-4070-add7-4b13bbfd7cb8");
		public readonly static Guid OsInstallGuid = new Guid("d16d444c-56d8-11d5-882d-0080c847b195");
	}

	[ComVisible(false)]
	public static class AssemblyCache
	{
		public static void InstallAssembly(String assemblyPath, InstallReference reference, AssemblyCommitFlags flags)
		{
			if (reference != null)
			{
				if (!InstallReferenceGuid.IsValidGuidScheme(reference.GuidScheme))
					throw new ArgumentException("Invalid reference guid.", "guid");
			}

			IAssemblyCache ac = null;

			int hr = 0;

			hr = Utils.CreateAssemblyCache(out ac, 0);
			if (hr >= 0)
			{
				hr = ac.InstallAssembly((int)flags, assemblyPath, reference);
			}

			if (hr < 0)
			{
				Marshal.ThrowExceptionForHR(hr);
			}
		}

		// assemblyName has to be fully specified name. 
		// A.k.a, for v1.0/v1.1 assemblies, it should be "name, Version=xx, Culture=xx, PublicKeyToken=xx".
		// For v2.0 assemblies, it should be "name, Version=xx, Culture=xx, PublicKeyToken=xx, ProcessorArchitecture=xx".
		// If assemblyName is not fully specified, a random matching assembly will be uninstalled. 
		public static void UninstallAssembly(String assemblyName, InstallReference reference, out AssemblyCacheUninstallDisposition disp)
		{
			AssemblyCacheUninstallDisposition dispResult = AssemblyCacheUninstallDisposition.Uninstalled;
			if (reference != null)
			{
				if (!InstallReferenceGuid.IsValidGuidScheme(reference.GuidScheme))
					throw new ArgumentException("Invalid reference guid.", "guid");
			}

			IAssemblyCache ac = null;

			int hr = Utils.CreateAssemblyCache(out ac, 0);
			if (hr >= 0)
			{
				hr = ac.UninstallAssembly(0, assemblyName, reference, out dispResult);
			}

			if (hr < 0)
			{
				Marshal.ThrowExceptionForHR(hr);
			}

			disp = dispResult;
		}

		// See comments in UninstallAssembly
		public static String QueryAssemblyInfo(String assemblyName)
		{
			if (assemblyName == null)
			{
				throw new ArgumentException("Invalid name", "assemblyName");
			}

			AssemblyInfo aInfo = new AssemblyInfo();

			aInfo.cchBuf = 1024;
			// Get a string with the desired length
			aInfo.currentAssemblyPath = new String('\0', aInfo.cchBuf);

			IAssemblyCache ac = null;
			int hr = Utils.CreateAssemblyCache(out ac, 0);
			if (hr >= 0)
			{
				hr = ac.QueryAssemblyInfo(0, assemblyName, ref aInfo);
			}
			if (hr < 0)
			{
				Marshal.ThrowExceptionForHR(hr);
			}

			return aInfo.currentAssemblyPath;
		}
	}

	[ComVisible(false)]
	public class AssemblyCacheEnum
	{
		// null means enumerate all the assemblies
		public AssemblyCacheEnum(String assemblyName)
		{
			IAssemblyName fusionName = null;
			int hr = 0;

			if (assemblyName != null)
			{
				hr = Utils.CreateAssemblyNameObject(
						out fusionName,
						assemblyName,
						CreateAssemblyNameObjectFlags.CANOF_PARSE_DISPLAY_NAME,
						IntPtr.Zero);
			}

			if (hr >= 0)
			{
				hr = Utils.CreateAssemblyEnum(
						out m_AssemblyEnum,
						IntPtr.Zero,
						fusionName,
						AssemblyCacheFlags.GAC,
						IntPtr.Zero);
			}

			if (hr < 0)
			{
				Marshal.ThrowExceptionForHR(hr);
			}
		}

		public String GetNextAssembly()
		{
			int hr = 0;
			IAssemblyName fusionName = null;

			if (done)
			{
				return null;
			}

			// Now get next IAssemblyName from m_AssemblyEnum
			hr = m_AssemblyEnum.GetNextAssembly((IntPtr)0, out fusionName, 0);

			if (hr < 0)
			{
				Marshal.ThrowExceptionForHR(hr);
			}

			if (fusionName != null)
			{
				return GetFullName(fusionName);
			}
			else
			{
				done = true;
				return null;
			}
		}

		private String GetFullName(IAssemblyName fusionAsmName)
		{
			StringBuilder sDisplayName = new StringBuilder(1024);
			int iLen = 1024;

			int hr = fusionAsmName.GetDisplayName(sDisplayName, ref iLen, (int)AssemblyNameDisplayFlags.ALL);
			if (hr < 0)
			{
				Marshal.ThrowExceptionForHR(hr);
			}

			return sDisplayName.ToString();
		}

		private IAssemblyEnum m_AssemblyEnum = null;
		private bool done;
	}// class AssemblyCacheEnum

	public class AssemblyCacheInstallReferenceEnum
	{
		public AssemblyCacheInstallReferenceEnum(String assemblyName)
		{
			IAssemblyName fusionName = null;

			int hr = Utils.CreateAssemblyNameObject(
						out fusionName,
						assemblyName,
						CreateAssemblyNameObjectFlags.CANOF_PARSE_DISPLAY_NAME,
						IntPtr.Zero);

			if (hr >= 0)
			{
				hr = Utils.CreateInstallReferenceEnum(out refEnum, fusionName, 0, IntPtr.Zero);
			}

			if (hr < 0)
			{
				Marshal.ThrowExceptionForHR(hr);
			}
		}

		public InstallReference GetNextReference()
		{
			IInstallReferenceItem item = null;
			int hr = refEnum.GetNextInstallReferenceItem(out item, 0, IntPtr.Zero);
			if ((uint)hr == 0x80070103)
			{   // ERROR_NO_MORE_ITEMS
				return null;
			}

			if (hr < 0)
			{
				Marshal.ThrowExceptionForHR(hr);
			}

			IntPtr refData;
			InstallReference instRef = new InstallReference(Guid.Empty, String.Empty, String.Empty);

			hr = item.GetReference(out refData, 0, IntPtr.Zero);
			if (hr < 0)
			{
				Marshal.ThrowExceptionForHR(hr);
			}

			Marshal.PtrToStructure(refData, instRef);
			return instRef;
		}

		private IInstallReferenceEnum refEnum;
	}

	internal class Utils
	{
		[DllImport("fusion.dll")]
		internal static extern int CreateAssemblyEnum(
				out IAssemblyEnum ppEnum,
				IntPtr pUnkReserved,
				IAssemblyName pName,
				AssemblyCacheFlags flags,
				IntPtr pvReserved);

		[DllImport("fusion.dll")]
		internal static extern int CreateAssemblyNameObject(
				out IAssemblyName ppAssemblyNameObj,
				[MarshalAs(UnmanagedType.LPWStr)]
                String szAssemblyName,
				CreateAssemblyNameObjectFlags flags,
				IntPtr pvReserved);

		[DllImport("fusion.dll")]
		internal static extern int CreateAssemblyCache(
				out IAssemblyCache ppAsmCache,
				int reserved);

		[DllImport("fusion.dll")]
		internal static extern int CreateInstallReferenceEnum(
				out IInstallReferenceEnum ppRefEnum,
				IAssemblyName pName,
				int dwFlags,
				IntPtr pvReserved);
	}
}
